package org.activequant.tradesystem.ib;

import java.util.concurrent.atomic.AtomicBoolean;

import org.activequant.broker.IBroker;
import org.activequant.broker.IOrderTracker;
import org.activequant.core.domainmodel.account.Order;
import org.activequant.core.domainmodel.events.OrderCompletionEvent;
import org.activequant.core.domainmodel.events.OrderEvent;
import org.activequant.core.types.OrderSide;
import org.activequant.core.types.OrderType;
import org.activequant.util.messaging.ISimpleMessagingService;
import org.activequant.util.pattern.events.IEventListener;
import org.apache.log4j.Logger;

/**
 * Opens a position, and if executed, protects it with the trailing stop
 * at a specified distance. Optionally (if configured) may communicate
 * the entry/exit info to the messenger service.<p>
 * SimplePositionService implements IPositionService.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>broker(IBroker)</li>
 * <li>distance(double)</li>
 * <li>msg(ISimpleMessagingService)</li>
 * </ul>
 * <b>History:</b><br>
 *  - [16.12.2007] Created (Mike Kroutikov)<br>
 *
 *  @author Mike Kroutikov
 */
public class SimplePositionService implements IPositionService {
	private final Logger log = Logger.getLogger(getClass());
	/**
	 * private final IBroker broker;
	 */
	private final IBroker broker;
	/**
	 * private double distance;
	 */
	private double distance;
	/**
	 * private ISimpleMessagingService msg;
	 */
	private ISimpleMessagingService msg;
	/**
	 * constructs a SimplePositionService(implements IPositionService) using the given broker(IBroker) to set its associated broker(IBroker)
	 * @param broker
	 */
	public SimplePositionService(IBroker broker) {
		this.broker = broker;
	}
	/**
	 * Sets the associated distance(double) with the given val(double)
	 * @param val
	 */
	public void setTrailingDistance(double val) {
		distance = val;
	}
	/**
	 * Sets the associated msg(ISimpleMessagingService) with the given val(ISimpleMessagingService)
	 * @param val
	 */
	public void setMessengerService(ISimpleMessagingService val) {
		msg = val;
	}
	/**
	 * If the given side(OrderSide) enum is BUY, returns an OrderSide.SELL enum.<br/>
	 * Otherwise, if the given side(OrderSide) enum is SELL, returns an OrderSide.BUY enum.<br/>
	 * Doesn't support any other cases
	 * @param side
	 * @return
	 */
	private static OrderSide reverseSide(OrderSide side) {
		switch(side) {
		case BUY: return OrderSide.SELL;
		case SELL: return OrderSide.BUY;
		default: throw new IllegalArgumentException("unsupported order side: " + side);
		}
	}
	/**
	 * private final static double EPSILON = 1e-6;
	 */
	private final static double EPSILON = 1e-6;
	/**
	 * Returns whether the absolute difference between the given d1(double) and d2(double) is less than the given epsilon(double)<br/>
	 * <code>return Math.abs(d1 - d2) < epsilon;</code>
	 * @param d1
	 * @param d2
	 * @param epsilon
	 * @return
	 */
	private static boolean doubleEquals(double d1, double d2, double epsilon) {
		return Math.abs(d1 - d2) < epsilon;
	}
	/**
	 * A private nested class. Tracker implements IPositionTracker.<br/>
	 * Holds the following associated variables:
	 * <ul>
	 * <li>originalOrder(Order)</li>
	 * <li>quantity(double)</li>
	 * <li>price(double)</li>
	 * <li>isClosed(AtomicBoolean)</li>
	 * <li>marketOrderTracker(IOrderTracker)</li>
	 * <li>marketOrderListener(IEventListener&lt;OrderEvent&gt;)</li>
	 * <li>stopOrderTracker(IOrderTracker)</li>
	 * <li>stopOrderListener(IEventListener&lt;OrderEvent&gt;)</li>
	 * </ul>
	 * @author Dimitar
	 *
	 */
	private class Tracker implements IPositionTracker {
		/**
		 * private final Order originalOrder;
		 */
		private final Order originalOrder;
		/**
		 * private double quantity;<br/>
		 * actual quantity of opened position (as per market order execution)
		 */
		private double quantity;
		/**
		 * private double price;<br/>
		 * actual price of opened position
		 */
		private double price;
		/**
		 * private final AtomicBoolean isClosed = new AtomicBoolean();
		 */
		private final AtomicBoolean isClosed = new AtomicBoolean();
		/**
		 * private final IOrderTracker marketOrderTracker;
		 */
		private final IOrderTracker marketOrderTracker;
		/**
		 * private final IEventListener&lt;OrderEvent&gt; marketOrderListener = new IEventListener&lt;OrderEvent&gt;() {...}<br/>
		 * A listener whose job is to react on OrderCompletionEvents only. Once it receives a completion(OrderCompletionEvent),
		 * it sets the associated quantity(double) and price(double) with the totalQuantity(double) and averagePrice(double) of that completion(OrderCompletionEvent)<br/>
		 * Then it tries to open a TRAILING_STOP Order to lock in any profits:<br/>
		 * -If no position has been opened already, returns without doing anything else.<br/>
		 * -Otherwise, if we already asked to close the position (and it exists) it will create/submit a MARKET Order to get rid of the position.<br/>
		 * -Otherwise, everything is good and it creates.submits a TRAILING_STOP with trailingDistance=${distance} to lock in the profits if price starts dropping
		 */
		private final IEventListener<OrderEvent> marketOrderListener = new IEventListener<OrderEvent>() {
			public void eventFired(OrderEvent event) {
				if (event instanceof OrderCompletionEvent) {
					// here the position has been opened
					OrderCompletionEvent completion = (OrderCompletionEvent) event;
					quantity = completion.getTotalQuantity();
					price = completion.getAveragePrice();
					placeTrailingStopOrder();
				}
			}
		};
		/**
		 * private IOrderTracker stopOrderTracker;
		 */
		private IOrderTracker stopOrderTracker;
		/**
		 * private final IEventListener&lt;OrderEvent&gt; stopOrderListener = new IEventListener&lt;OrderEvent&gt;() {...}<br/>
		 * A listener whose job is to wait for an OrderCompletionEvent whose total totalQuantity matches the associated quantity(double) (up to a very small difference is ok).<br/>
		 * If they match we have happily closed the position, it sets the isClosed(AtomicBoolean) to true and sends a short report to the user.<br/>
		 * Otherwise it sends the bad news "shark alert" message to the user since it didn't get a full position close.
		 * 
		 */
		private final IEventListener<OrderEvent> stopOrderListener = new IEventListener<OrderEvent>() {
			/**
			 * <strong>1.</strong> The given event(OrderEvent) has to be an OrderCompletionEvent, otherwise it returns without doing anything.<br/>
			 * <strong>2.</strong> Gets completion(OrderCompletionEvent) by casting the given event(OrderEvent) into an OrderCompletionEvent<br/>
			 * <strong>3.</strong>If the difference between the associated quantity(double) and the totalQuantity(double) of the completion(OrderCompletionEvent) is 
			 * small enough that we can say the are identical:<br/>
			 * - sets the value of the associated isClosed(AtomicBoolean) to true<br/>
			 * - if the msg(ISimpleMessagingService) is not null, it uses it to send a message whose subj="position closed" and a body in the form:<br/>
			 * <strong>closed<br/>
			 * ${originalOrder}<br/>
			 * averagePrice=${completion.averagePrice}</strong><br/>
			 * <strong>4.</strong> Otherwise, If the difference is not that small (we failed to close the order completely or partially):<br/>
			 * - if the msg(ISimpleMessagingService) is not null, it uses it to send a message whose subj="shark alert" and a body in the form:<br/>
			 * <strong>failed to close position<br/>
			 * ${originalOrder}</strong>
			 * @param e
			 */
			public void eventFired(OrderEvent event) {
				if(!(event instanceof OrderCompletionEvent)) {
					return;
				}
				OrderCompletionEvent completion = (OrderCompletionEvent) event;
				if(doubleEquals(quantity, completion.getTotalQuantity(), EPSILON)) {
					// all ok, position has been closed
					isClosed.set(true);
					log.info("closed position: " + originalOrder);
					if(msg != null) {
						msg.sendMessage("position closed", "closed\n" + originalOrder + "\n" + "averagePrice=" + completion.getAveragePrice());
					}
				} else {//doesn't handle partial execution
					// oops. failed to close!
					// abnormal condition. cry for human intervention
					log.error("shark alert: failed to close position: " + originalOrder);
					if(msg != null) {
						msg.sendMessage("shark alert", "failed to close position\n" + originalOrder);
					}
				}
			}
		};
		/**
		 * If no position has been opened already, returns without doing anything else.<br/>
		 * Otherwise, if we already asked to close the position (and it exists) it will create/submit a MARKET Order to get rid of the position.<br/>
		 * Otherwise, everything is good and it creates/submits a TRAILING_STOP with trailingDistance=${distance} to lock in the profits if price starts dropping:<br/>
		 * <strong>1.</strong> If the associated quantity(double) is 0, it sets the value of the associated isClosed(AtomicBoolean) to true 
		 * and returns without doing anything else (a position wasn't even opened in this case, no reason to continue) <br/>
		 * <strong>2.</strong> If the msg(ISimpleMessagingService) is not null, it uses it to send a message whose subj="position opened" and a body in the form:<br/>
		 * <strong>opened: quantity=${quantity}, price=${price}<br/>
		 * ${originalOrder}</strong><br/>
		 * <strong>3.</strong> If the associated value of the associated isClosed(AtomicBoolean) id true:<br/>
		 * - if the associated quantity(double) is 0 or less, it returns without doing anything else.<br/>
		 * -Creates a MARKET order(Order) whose instrumentSpecification(InstrumentSpecification) is same as the one in the associated originalOrder(Order), an opposite OrderSide 
		 * from the one of the associated originalOrder(Order), and for the associated quantity(double).<br/>
		 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated stopOrderTracker(IOrderTracker) 
		 * with the resulting IOrderTracker.<br/>
		 * - registers the associated stopOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated stopOrderTracker(IOrderTracker) <br/>
		 * - submits the order by invoking <code>stopOrderTracker.submit()</code> on the associated stopOrderTracker(IOrderTracker) and then returns without doing anything else.<br/>
		 * <strong>4.</strong> Otherwise (everything is normal), creates a TRAILING_STOP order(Order) whose instrumentSpecification(InstrumentSpecification) is same as the one in the associated originalOrder(Order)
		 * , an opposite OrderSide from the one of the associated originalOrder(Order), and for the associated quantity(double). Its trailingDistance(double) is set with the distance(double).<br/>
		 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated stopOrderTracker(IOrderTracker) 
		 * with the resulting IOrderTracker.<br/>
		 * - registers the associated stopOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated stopOrderTracker(IOrderTracker) <br/>
		 * - submits the order by invoking <code>stopOrderTracker.submit()</code> on the associated stopOrderTracker(IOrderTracker).
		 */
		private void placeTrailingStopOrder() {
			if(quantity == 0.0) {
				// position was not opened, see logs for more details
				isClosed.set(true);
				return;
			}
			log.info("opened position: quantity=" + quantity + ", price=" + price + "," + originalOrder);
			if(msg != null) {
				msg.sendMessage("position opened", "opened: quantity=" + quantity + ", price=" + price + "\n" + originalOrder);
			}
			if(isClosed.get()) {
				// position has been closed (requested to be closed)
				// before we had a chance to issue trailing stop
				// if any outstanding amount, lets close it
				if(quantity <= 0.0) {
					return;
				}
				Order order = new Order();
				order.setOrderType(OrderType.MARKET);
				order.setInstrumentSpecification(originalOrder.getInstrumentSpecification());
				order.setQuantity(quantity);
				order.setOrderSide(reverseSide(originalOrder.getOrderSide()));
				stopOrderTracker = broker.prepareOrder(order);
				stopOrderTracker.getOrderEventSource().addEventListener(stopOrderListener);
				stopOrderTracker.submit();
				return;
			}
			// normal stuff: position opened, lets put a trailing stop
			Order order = new Order();
			order.setOrderType(OrderType.TRAILING_STOP);
			order.setInstrumentSpecification(originalOrder.getInstrumentSpecification());
			order.setQuantity(quantity);
			order.setTrailingDistance(distance);
			order.setOrderSide(reverseSide(originalOrder.getOrderSide()));
			stopOrderTracker = broker.prepareOrder(order);
			stopOrderTracker.getOrderEventSource().addEventListener(stopOrderListener);
			stopOrderTracker.submit();
		}
		/**
		 * Constructs a Tracker(implements IPositionTracker) using the given order(Order) to set its associated originalOrder(Order) (and submits the given order(Order)):<br/>
		 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated marketOrderTracker(IOrderTracker) with the resulting IOrderTracker.<br/>
		 * - registers the associated marketOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated marketOrderTracker(IOrderTracker) <br/>
		 * - submits the order by invoking <code>marketOrderTracker.submit()</code> on the associated marketOrderTracker(IOrderTracker).
		 * @param order
		 */
		public Tracker(Order order) {
			originalOrder = order;
			marketOrderTracker = broker.prepareOrder(order);
			marketOrderTracker.getOrderEventSource().addEventListener(marketOrderListener);
			marketOrderTracker.submit();
		}
		/**
		 * <strong>1.</strong> Gets the previous value(boolean) of the associated isClosed(AtomicBoolean) and sets its new value to true.<br/>
		 * If that previous value(boolean) is true, it returns without doing anything else.<br/>
		 * <strong>2.</strong> Tries to cancel (if not filled yet) the associated originalOrder(Order) by invoking <code>marketOrderTracker.cancel()</code> on the associated marketOrderTracker(IOrderTracker).<br/>
		 * <strong>3.</strong> If the associated quantity(double) is greater than 0, it gets the TRAILING_STOP Order from the associated stopOrderTracker(IOrderTracker), sets its
		 * trailingDistance(double) to 0, and updates the pending order by invoking <code>stopOrderTracker.update(order)</code> on the associated stopOrderTracker(IOrderTracker)
		 */
		public void close() {
			if(isClosed.getAndSet(true)) {
				return; // already closed
			}
			// if market order not yet filled, cancel it...
			marketOrderTracker.cancel();
			if(quantity > 0.0) {
				// have to close the position. Lets do it by moving
				// trailing distance to zero
				Order order = new Order(stopOrderTracker.getOrder());
				order.setTrailingDistance(0.0);
				stopOrderTracker.update(order);
			}
		}
		/**
		 * returns the current value of the associated isClosed(AtomicBoolean)
		 */
		public boolean isClosed() {
			return isClosed.get();
		}
		/**
		 * {@inheritDoc}
		 */
		public double getQuantity() {
			// TODO Auto-generated method stub
			throw new UnsupportedOperationException();
		}
	}
	/**
	 * Creates and returns a Tracker(implements IPositionTracker) using the given order(Order) to set its associated originalOrder(Order) (and submits the given order(Order) asynchronously):<br/>
	 * - validates and prepares that order(Order) for submission by invoking <code>broker.prepareOrder(order)</code> on the broker(IBroker) and sets the associated marketOrderTracker(IOrderTracker) with the resulting IOrderTracker.<br/>
	 * - registers the associated marketOrderListener(IEventListener&lt;OrderEvent&gt;) to listen for OrderEvents from the associated marketOrderTracker(IOrderTracker) <br/>
	 * - submits the order by invoking <code>marketOrderTracker.submit()</code> on the associated marketOrderTracker(IOrderTracker).
	 */
	public IPositionTracker openPosition(Order order) {
		return new Tracker(new Order(order));
	}
}
